/**
 * 所谓不动笔墨不读书
 * 又来读一读理解进程内存排布，掌握程序动态
 * 
 * 进程的虚拟空间排布
 * 
 * ELF文件头中指定的程序入口地址，各个节区在程序运行时的内存排布地址等，都是进程虚拟空间中的地址
 * 
 * 虚拟空间可以认为是操作系统给每个进程准备的沙盒。
 * 每个进程只存活在自己的虚拟世界里，却感觉自己独占了所有的系统资源
 * 
 * 降低每个进程内存管理的复杂度，进程只需关心如何使用自己线性排列的虚拟地址，而不需关系无力内存的实际容量，以及如何使用真实的物理内存
 * 
 * 虚拟空间地址排布
 * 32位 虚拟空间4G 1G给内核空间，3G给用户
 * 64位，虚拟空间可达256TB，内核和应用分别占用128TB
 * 
 * 加载器会为每个应用分配栈区、堆区和动态链接库加载区
 * 栈和堆分别向相对方向增长
 * 
 * cat /proc/PID/maps
 * 可以查看内存的进程排布 可以看到进程的段，堆区，动态链接库加载区，栈区的逻辑地址排布，以及每块内存区分配到的权限等
 * vdso和vsyscall内存区，是一部分的内核数据在用户空间的映射，为了提高应用的性能而创建。
 * 
 * 进程的启动
 * 开发人员 系统调用 fork()和execve()
 * fork创建新进程 在内核中，代码段所在的只读存储区，会共享相同的物理内存页，可读可写的数据段、堆以及栈等内存，内存会使用写时拷贝技术，为每个进程独立创建一份
 * 
 * fork之后，文件描述符的引用计数增加，关闭一个进程，也只是减少引用计数，无法彻底关闭文件，要关闭只能所有进程都关闭
 * 
 * execve系统调用
 * 运行另外一个指定的程序。会把新程序加载到当前进程的内存空间内，当前的进程会被丢弃，堆栈和所有的段数据都会被新进程相应的部分代替，然后会从新程序的初始化代码和main函数开始运行
 * 
 * execve系统调用常常和fork一起配合使用。从一个进程中启动另一个程序时，首先fork，然后在子进程中使用execve变身为运行制定程序的进程
 * 
 * ***/

int execve(const char *filename,char *const argv[],char *const envp[]);

/**
 * filename用于指定要运行的程序的文件名，argv和envp分别指定程序的运行参数和环境变量
 * 该函数还有很多变体execl、execlp、execle、execv、execvp、execvpe 
 * 
 * exec系列函数的返回值只有遇到错误才会有意义。如果新程序成功执行，那么当前进程的所有数据都会被新进程替换掉，所以就没有了返回值
 * 
 * exec函数的应用，总是使用内核为文件提供的执行时关闭标志(FD_CLOEXEC)
 * 设置该标志，若exec成功，文件就会被自动关闭，失败就保持打开 fcntl可以设置该标志
 * 
 * fexecve()函数，区别于execve，第一个参数使用的是打开的文件描述符，而非路径名
 * 增加这个函数是为了满足，验证文件是否被恶意修改 更佳安全
 * 
 * ***/

/**
 * 监控子进程状态
 * 在Linux应用中，父进程需要监控其创建的所有子进程的退出状态
 * 
 * pid_t wait(int *status);
 * 一直阻塞等待任意一个子进程退出，返回值为退出的子进程的ID，status中包含子进程设置的退出标志
 * 
 * pid_t waitpid(pid_t pid,int *status,int options)
 * 等待指定进程或进程组ID，options可以控制是否阻塞以及是否监控因信号而停止的子进程等
 * 
 * int waittid(idtype,id_t,siginfo_t*,int options);
 * 更精细的控制选项来监控指定子进程的运行状态
 * 
 * 僵尸进程，在子进程终止到父进程获取退出状态之间的这段时间。
 * 
 * 
 * 进程的终止
 * 
 * void _exit(int status); 系统调用
 * 
 * void exit(int status); 库函数
 * 先退出处理程序，然后再利用status参数调用_exit()系统调用
 * 
 * 个性化退出处理函数
 * int atexit(void (*func)(void));
 * int on_exit(void (*func)(int, void *), void *arg);
 * 
 * 在主进程执行一次，一般在主进程执行，子进程用_exit()
 * 
 * ***/